<!DOCTYPE html>
<html lang="zh-CN">
  <head hexo-theme='https://github.com/volantis-x/hexo-theme-volantis/tree/4.3.1'>
  <meta charset="utf-8">
  <!-- SEO相关 -->
  
    
  
  <!-- 渲染优化 -->
  <meta http-equiv='x-dns-prefetch-control' content='on' />
  <link rel='dns-prefetch' href='https://cdn.jsdelivr.net'>
  <link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
  <meta name="renderer" content="webkit">
  <meta name="force-rendering" content="webkit">
  <meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1">
  <meta name="HandheldFriendly" content="True" >
  <meta name="apple-mobile-web-app-capable" content="yes">
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
  
  <link rel="preload" href="/blog/css/first.css" as="style">
  

  <!-- 页面元数据 -->
  
  <title>React 是什么？ - song的个人博客</title>
  
    <meta name="keywords" content="React">
  

  

  <!-- feed -->
  

  <!-- import meta -->
  
    
      <meta name="msapplication-TileColor" content="#ffffff">
    
      <meta name="msapplication-config" content="https://cdn.jsdelivr.net/gh/volantis-x/cdn-org/blog/favicon/browserconfig.xml">
    
      <meta name="theme-color" content="#ffffff">
    
  

  <!-- link -->
  
    <link rel="shortcut icon" type='image/x-icon' href="https://cdn.jsdelivr.net/gh/xaoxuu/assets@master/favicon/favicon.ico">
  

  <!-- import link -->
  
  
  <link rel="apple-touch-icon" sizes="180x180" href="https://cdn.jsdelivr.net/gh/volantis-x/cdn-org/blog/favicon/apple-touch-icon.png">
  <link rel="icon" type="image/png" sizes="32x32" href="https://cdn.jsdelivr.net/gh/volantis-x/cdn-org/blog/favicon/favicon-32x32.png">
  <link rel="icon" type="image/png" sizes="16x16" href="https://cdn.jsdelivr.net/gh/volantis-x/cdn-org/blog/favicon/favicon-16x16.png">
  <link rel="manifest" href="https://cdn.jsdelivr.net/gh/volantis-x/cdn-org/blog/favicon/site.webmanifest">
  <link rel="mask-icon" href="https://cdn.jsdelivr.net/gh/volantis-x/cdn-org/blog/favicon/safari-pinned-tab.svg" color="#5bbad5">
  <link rel="shortcut icon" href="https://cdn.jsdelivr.net/gh/volantis-x/cdn-org/blog/favicon/favicon.ico">
  

  
    
<link rel="stylesheet" href="/blog/css/first.css">

  

  
  <link rel="stylesheet" href="/blog/css/style.css" media="print" onload="this.media='all';this.onload=null">
  <noscript><link rel="stylesheet" href="/blog/css/style.css"></noscript>
  

  <script id="loadcss"></script>

  
<script>
if (/*@cc_on!@*/false || (!!window.MSInputMethodContext && !!document.documentMode))
    document.write(
	'<style>'+
		'html{'+
			'overflow-x: hidden !important;'+
			'overflow-y: hidden !important;'+
		'}'+
		'.kill-ie{'+
			'text-align:center;'+
			'height: 100%;'+
			'margin-top: 15%;'+
			'margin-bottom: 5500%;'+
		'}'+
	'</style>'+
    '<div class="kill-ie">'+
        '<h1><b>抱歉，您的浏览器无法访问本站</b></h1>'+
        '<h3>微软已经于2016年终止了对 Internet Explorer (IE) 10 及更早版本的支持，<br/>'+
        '继续使用存在极大的安全隐患，请使用当代主流的浏览器进行访问。</h3><br/>'+
        '<a target="_blank" rel="noopener" href="https://www.microsoft.com/zh-cn/WindowsForBusiness/End-of-IE-support"><strong>了解详情 ></strong></a>'+
    '</div>');
</script>


<noscript>
	<style>
		html{
			overflow-x: hidden !important;
			overflow-y: hidden !important;
		}
		.kill-noscript{
			text-align:center;
			height: 100%;
			margin-top: 15%;
			margin-bottom: 5500%;
		}
	</style>
    <div class="kill-noscript">
        <h1><b>抱歉，您的浏览器无法访问本站</b></h1>
        <h3>本页面需要浏览器支持（启用）JavaScript</h3><br/>
        <a target="_blank" rel="noopener" href="https://www.baidu.com/s?wd=启用JavaScript"><strong>了解详情 ></strong></a>
    </div>
</noscript>

</head>

  <body>
    

<header id="l_header" class="l_header auto shadow blur show" style='opacity: 0' >
  <div class='container'>
  <div id='wrapper'>
    <div class='nav-sub'>
      <p class="title"></p>
      <ul class='switcher nav-list-h m-phone' id="pjax-header-nav-list">
        <li><a id="s-comment" class="fas fa-comments fa-fw" target="_self" href='javascript:void(0)'></a></li>
        
          <li><a id="s-toc" class="s-toc fas fa-list fa-fw" target="_self" href='javascript:void(0)'></a></li>
        
      </ul>
    </div>
		<div class="nav-main">
      
        
        <a class="title flat-box" target="_self" href='/blog/'>
          
            <img no-lazy class='logo' src='https://cdn.jsdelivr.net/gh/volantis-x/cdn-org/blog/Logo-NavBar@3x.png'/>
          
          
          
        </a>
      

			<div class='menu navigation'>
				<ul class='nav-list-h m-pc'>
          
          
          
            
            
              <li>
                <a class="menuitem flat-box faa-parent animated-hover" href=/blog/
                  
                  
                  
                    id="blog"
                  >
                  <i class='fas fa-rss fa-fw'></i>博客
                </a>
                
              </li>
            
          
          
				</ul>
			</div>

      <div class="m_search">
        <form name="searchform" class="form u-search-form">
          <i class="icon fas fa-search fa-fw"></i>
          <input type="text" class="input u-search-input" placeholder="Search..." />
        </form>
      </div>

			<ul class='switcher nav-list-h m-phone'>
				
					<li><a class="s-search fas fa-search fa-fw" target="_self" href='javascript:void(0)'></a></li>
				
				<li>
          <a class="s-menu fas fa-bars fa-fw" target="_self" href='javascript:void(0)'></a>
          <ul class="menu-phone list-v navigation white-box">
            
              
            
              <li>
                <a class="menuitem flat-box faa-parent animated-hover" href=/blog/
                  
                  
                  
                    id="blog"
                  >
                  <i class='fas fa-rss fa-fw'></i>博客
                </a>
                
              </li>
            
          
            
          </ul>
        </li>
			</ul>
		</div>
	</div>
  </div>
</header>

    <div id="l_body">
      <div id="l_cover">
  
    
        <div id="full" class='cover-wrapper py dock' style="display: none;">
          
            <div class='cover-bg lazyload placeholder' data-bg="https://bing.ioliu.cn/v1/rand?w=1920&h=1200"></div>
          
          <div class='cover-body'>
  <div class='top'>
    
    
      <p class="title">song的个人博客</p>
    
    
  </div>
  <div class='bottom'>
    <div class='menu navigation'>
      <div class='list-h'>
        
          
            <a href="/blog/v4/getting-started/"
              
              
              id="blogv4getting-started">
              <img src='https://cdn.jsdelivr.net/gh/twitter/twemoji@13.0/assets/svg/1f5c3.svg'><p>文档</p>
            </a>
          
            <a href="/blog/faqs/"
              
              
              id="blogfaqs">
              <img src='https://cdn.jsdelivr.net/gh/twitter/twemoji@13.0/assets/svg/1f516.svg'><p>帮助</p>
            </a>
          
            <a href="/blog/examples/"
              
              
              id="blogexamples">
              <img src='https://cdn.jsdelivr.net/gh/twitter/twemoji@13.0/assets/svg/1f396.svg'><p>示例</p>
            </a>
          
            <a href="/blog/contributors/"
              
              
              id="blogcontributors">
              <img src='https://cdn.jsdelivr.net/gh/twitter/twemoji@13.0/assets/svg/1f389.svg'><p>社区</p>
            </a>
          
            <a href="/blog/archives/"
              
              
              id="blogarchives">
              <img src='https://cdn.jsdelivr.net/gh/twitter/twemoji@13.0/assets/svg/1f4f0.svg'><p>博客</p>
            </a>
          
            <a target="_blank" rel="noopener" href="https://github.com/volantis-x/hexo-theme-volantis/"
              
              
              id="https:githubcomvolantis-xhexo-theme-volantis">
              <img src='https://cdn.jsdelivr.net/gh/twitter/twemoji@13.0/assets/svg/1f9ec.svg'><p>源码</p>
            </a>
          
        
      </div>
    </div>
  </div>
</div>

          <div id="scroll-down" style="display: none;"><i class="fa fa-chevron-down scroll-down-effects"></i></div>
        </div>
    
  
  </div>

      <div id="safearea">
        <div class="body-wrapper" id="pjax-container">
          

<div class='l_main'>
  <article class="article post white-box reveal md shadow article-type-py" id="py" itemscope itemprop="blogPost">
  


  
    <div class='headimg-div'>
      <a class='headimg-a'>
        <img class='headimg' src='https://z3.ax1x.com/2021/10/30/5xiKXj.png'/>
      </a>
    </div>
  
  <div class="article-meta" id="top">
    
    
    
      <h1 class="title">
        React 是什么？
      </h1>
      <div class='new-meta-box'>
        
          
            
<div class='new-meta-item author'>
  <a class='author' href="/" rel="nofollow">
    <img no-lazy src="">
    <p>请设置文章作者</p>
  </a>
</div>

          
        
          
            
  <div class='new-meta-item category'>
    <a class='notlink'>
      <i class="fas fa-folder-open fa-fw" aria-hidden="true"></i>
      <a class="category-link" href="/blog/categories/React/">React</a>
    </a>
  </div>


          
        
          
            <div class="new-meta-item date">
  <a class='notlink'>
    <i class="fas fa-calendar-alt fa-fw" aria-hidden="true"></i>
    <p>发布于：2021年10月30日</p>
  </a>
</div>

          
        
          
            
  <div class="new-meta-item browse leancloud">
    <a class='notlink'>
      
      <div id="lc-pv" data-title="React 是什么？" data-path="/blog/javaReact.html">
        <i class="fas fa-eye fa-fw" aria-hidden="true"></i>
        <span id='number'><i class="fas fa-circle-notch fa-spin fa-fw" aria-hidden="true"></i></span>
        次浏览
      </div>
    </a>
  </div>


          
        
      </div>
    
  </div>


  
  
  <h2 id="React-是什么？"><a href="#React-是什么？" class="headerlink" title="React 是什么？"></a>React 是什么？</h2><p>React 是一个声明式，高效且灵活的用于构建用户界面的 JavaScript 库。使用 React 可以将一些简短、独立的代码片段组合成复杂的 UI 界面，这些代码片段被称作“组件”。</p>
<p>React 中拥有多种不同类型的组件，我们先从 <code>React.Component</code> 的子类开始介绍：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">class ShoppingList extends React.Component &#123;</span><br><span class="line">  render() &#123;</span><br><span class="line">    return (</span><br><span class="line">      &lt;div className=&quot;shopping-list&quot;&gt;</span><br><span class="line">        &lt;h1&gt;Shopping List for &#123;this.props.name&#125;&lt;/h1&gt;</span><br><span class="line">        &lt;ul&gt;</span><br><span class="line">          &lt;li&gt;Instagram&lt;/li&gt;</span><br><span class="line">          &lt;li&gt;WhatsApp&lt;/li&gt;</span><br><span class="line">          &lt;li&gt;Oculus&lt;/li&gt;</span><br><span class="line">        &lt;/ul&gt;</span><br><span class="line">      &lt;/div&gt;</span><br><span class="line">    );</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line">// 用法示例: &lt;ShoppingList name=&quot;Mark&quot; /&gt;</span><br></pre></td></tr></table></figure>

<p>ShoppingList 是一个 <strong>React 组件类</strong>，或者说是一个 <strong>React 组件类型</strong>。</p>
<p>一个组件接收一些参数，我们把这些参数叫做 <code>props</code>（“props” 是 “properties” 简写），然后通过 <code>render</code> 方法返回需要展示在屏幕上的视图的层次结构。</p>
<p><code>render</code> 方法的返回值<em>描述</em>了你希望在屏幕上看到的内容。React 根据描述，然后把结果展示出来。更具体地来说，<code>render</code> 返回了一个 <strong>React 元素</strong></p>
<h3 id="JXS"><a href="#JXS" class="headerlink" title="JXS"></a>JXS</h3><p>大多数的 React 开发者使用了一种名为 “JSX” 的特殊语法，JSX 可以让你更轻松地书写这些结构。语法 <code>&lt;div /&gt;</code> 会被编译成 <code>React.createElement(&#39;div&#39;)</code>。上述的代码等同于：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">React.createElement(&quot;div&quot;, &#123; className: &quot;shopping-list&quot; &#125;,</span><br><span class="line">React.createElement(&quot;h1&quot;, null, &quot;Shopping List for &quot;, props.name), </span><br><span class="line">React.createElement(&quot;ul&quot;, null, </span><br><span class="line">	React.createElement(&quot;li&quot;, null, &quot;Instagram&quot;), </span><br><span class="line">	React.createElement(&quot;li&quot;, null, &quot;WhatsApp&quot;), </span><br><span class="line">	React.createElement(&quot;li&quot;, null, &quot;Oculus&quot;)</span><br><span class="line">	) /*#__PURE__*/</span><br><span class="line">); /*#__PURE__*/</span><br></pre></td></tr></table></figure>

<p>在 JSX 中你可以任意使用 JavaScript 表达式，只需要用一个大括号把表达式括起来。每一个 React 元素事实上都是一个 JavaScript 对象，你可以在你的程序中把它当保存在变量中或者作为参数传递。</p>
<p>前文中的 <code>ShoppingList</code> 组件只会渲染一些内置的 DOM 组件，如<code>&lt;div /&gt;</code>、<code>&lt;li /&gt;</code>等。</p>
<p>但是你也可以组合和渲染自定义的 React 组件。</p>
<p>例如，你可以通过 <code>&lt;ShoppingList /&gt;</code> 来表示整个购物清单组件。每个组件都是封装好的，并且可以单独运行，这样你就可以通过组合简单的组件来构建复杂的 UI 界面。</p>
<h3 id="通过-Props-传递数据"><a href="#通过-Props-传递数据" class="headerlink" title="通过 Props 传递数据"></a>通过 Props 传递数据</h3><p>让我们试试水，尝试将数据从 Board 组件传递到 Square 组件中。</p>
<p>我们强烈建议你动手编写本教程中的代码，不要使用复制/粘贴。这将加深你对 React 的记忆和理解。</p>
<p>在 Board 组件的 <code>renderSquare</code> 方法中，我们将代码改写成下面这样，传递一个名为 <code>value</code> 的 prop 到 Square 当中：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">class Board extends React.Component &#123;</span><br><span class="line">  renderSquare(i) &#123;</span><br><span class="line">    return &lt;Square value=&#123;i&#125; /&gt;;  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>修改 Square 组件中的 <code>render</code> 方法，把 <code>&#123;/* TODO */&#125;</code> 替换为 <code>&#123;this.props.value&#125;</code>，以显示上文中传入的值：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">class Square extends React.Component &#123;</span><br><span class="line">  render() &#123;</span><br><span class="line">    return (</span><br><span class="line">      &lt;button className=&quot;square&quot;&gt;</span><br><span class="line">        &#123;this.props.value&#125;      </span><br><span class="line">      &lt;/button&gt;</span><br><span class="line">    );</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>修改前：</p>
<p><a target="_blank" rel="noopener" href="https://react.docschina.org/static/1566a4f8490d6b4b1ed36cd2c11fe4b6/6a91e/tictac-empty.png"><img src="https://react.docschina.org/static/1566a4f8490d6b4b1ed36cd2c11fe4b6/6a91e/tictac-empty.png" class="lazyload" data-srcset="https://react.docschina.org/static/1566a4f8490d6b4b1ed36cd2c11fe4b6/6a91e/tictac-empty.png" srcset="" alt="React Devtools"></a></p>
<p>修改后：在渲染结果中，你可以看到每个方格中都有一个数字。</p>
<p><a target="_blank" rel="noopener" href="https://react.docschina.org/static/685df774da6da48f451356f33f4be8b2/01bf6/tictac-numbers.png"><img src="https://react.docschina.org/static/685df774da6da48f451356f33f4be8b2/01bf6/tictac-numbers.png" class="lazyload" data-srcset="https://react.docschina.org/static/685df774da6da48f451356f33f4be8b2/01bf6/tictac-numbers.png" srcset="" alt="React Devtools"></a></p>
<p><strong><a target="_blank" rel="noopener" href="https://codepen.io/gaearon/pen/aWWQOG?editors=0010">查看此步完整代码示例</a></strong></p>
<p>恭喜你！你刚刚成功地把一个 prop 从父组件 Board “传递”给了子组件 Square。在 React 应用中，数据通过 props 的传递，从父组件流向子组件。</p>
<h3 id=""><a href="#" class="headerlink" title=""></a></h3><h3 id="给组件添加交互功能"><a href="#给组件添加交互功能" class="headerlink" title="给组件添加交互功能"></a>给组件添加交互功能</h3><p>接下来我们试着让棋盘的每一个格子在点击之后能落下一颗 “X” 作为棋子。 首先，我们把 Square 组件中 <code>render()</code> 方法的返回值中的 button 标签修改为如下内容：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">class Square extends React.Component &#123;</span><br><span class="line">  render() &#123;</span><br><span class="line">    return (</span><br><span class="line">      &lt;button className=&quot;square&quot; onClick=&#123;function() &#123; alert(&#x27;click&#x27;); &#125;&#125;&gt;        </span><br><span class="line">      	&#123;this.props.value&#125;</span><br><span class="line">      &lt;/button&gt;</span><br><span class="line">    );</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>如果此刻点击某个格子，浏览器会弹出提示框。</p>
<blockquote>
<p>注意</p>
<p>为了少输入代码，同时为了避免 <a target="_blank" rel="noopener" href="https://yehudakatz.com/2011/08/11/understanding-javascript-function-invocation-and-this/"><code>this</code> 造成的困扰</a>，我们在这里使用<a target="_blank" rel="noopener" href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/Arrow_functions">箭头函数</a> 来进行事件处理，如下所示：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">class Square extends React.Component &#123;</span><br><span class="line"> render() &#123;</span><br><span class="line">   return (</span><br><span class="line">     &lt;button className=&quot;square&quot; onClick=&#123;() =&gt; alert(&#x27;click&#x27;)&#125;&gt;       </span><br><span class="line">     	&#123;this.props.value&#125;</span><br><span class="line">     &lt;/button&gt;</span><br><span class="line">   );</span><br><span class="line"> &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>注意：此处使用了 <code>onClick=&#123;() =&gt; alert(&#39;click&#39;)&#125;</code> 的方式向 <code>onClick</code> 这个 prop 传入一个<em>函数</em>。 React 将在单击时调用此函数。但很多人经常忘记编写 <code>() =&gt;</code>，而写成了 <code>onClick=&#123;alert(&#39;click&#39;)&#125;</code>，这种常见的错误会导致每次这个组件渲染的时候都会触发弹出框。</p>
</blockquote>
<p>接下来，我们希望 Square 组件可以“记住”它被点击过，然后用 “X” 来填充对应的方格。我们用 <strong>state</strong> 来实现所谓“记忆”的功能。</p>
<p>可以通过在 React 组件的构造函数中设置 <code>this.state</code> 来初始化 state。<code>this.state</code> 应该被视为一个组件的私有属性。我们在 <code>this.state</code> 中存储当前每个方格（Square）的值，并且在每次方格被点击的时候改变这个值。</p>
<p>首先，我们向这个 class 中添加一个构造函数，用来初始化 state：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">class Square extends React.Component &#123;</span><br><span class="line">  constructor(props) &#123;    super(props);    this.state = &#123;      value: null,    &#125;;  &#125;</span><br><span class="line">  render() &#123;</span><br><span class="line">    return (</span><br><span class="line">      &lt;button className=&quot;square&quot; onClick=&#123;() =&gt; alert(&#x27;click&#x27;)&#125;&gt;</span><br><span class="line">        &#123;this.props.value&#125;</span><br><span class="line">      &lt;/button&gt;</span><br><span class="line">    );</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<blockquote>
<p>注意</p>
<p>在 <a target="_blank" rel="noopener" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes">JavaScript class</a> 中，每次你定义其子类的构造函数时，都需要调用 <code>super</code> 方法。因此，在所有含有构造函数的的 React 组件中，构造函数必须以 <code>super(props)</code> 开头。</p>
</blockquote>
<p>现在，我们来修改一下 Square 组件的 <code>render</code> 方法，这样，每当方格被点击的时候，就可以显示当前 state 的值了：</p>
<ul>
<li>在 <code>&lt;button&gt;</code> 标签中，把 <code>this.props.value</code> 替换为 <code>this.state.value</code>。</li>
<li>将 <code>onClick=&#123;...&#125;</code> 事件监听函数替换为 <code>onClick=&#123;() =&gt; this.setState(&#123;value: &#39;X&#39;&#125;)&#125;</code>。</li>
<li>为了更好的可读性，将 <code>className</code> 和 <code>onClick</code> 的 prop 分两行书写。</li>
</ul>
<p>修改之后，Square 组件中 <code>render</code> 方法的返回值中的 <code>&lt;button&gt;</code> 标签就变成了下面这样：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">class Square extends React.Component &#123;</span><br><span class="line">  constructor(props) &#123;</span><br><span class="line">    super(props);</span><br><span class="line">    this.state = &#123;</span><br><span class="line">      value: null,</span><br><span class="line">    &#125;;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  render() &#123;</span><br><span class="line">    return (</span><br><span class="line">      &lt;button</span><br><span class="line">        className=&quot;square&quot;        </span><br><span class="line">        onClick=&#123;() =&gt; this.setState(&#123;value: &#x27;X&#x27;&#125;)&#125;      </span><br><span class="line">       &gt;</span><br><span class="line">        &#123;this.state.value&#125;      </span><br><span class="line">       &lt;/button&gt;</span><br><span class="line">    );</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>在 Square 组件 <code>render</code> 方法中的 <code>onClick</code> 事件监听函数中调用 <code>this.setState</code>，我们就可以在每次 <code>&lt;button&gt;</code> 被点击的时候通知 React 去重新渲染 Square 组件。组件更新之后，Square 组件的 <code>this.state.value</code> 的值会变为 <code>&#39;X&#39;</code>，因此，我们在游戏棋盘上就能看见 <code>X</code> 了。点击任意一个方格，<code>X</code> 就会出现了。</p>
<p>每次在组件中调用 <code>setState</code> 时，React 都会自动更新其子组件。</p>
<h3 id="开发者工具"><a href="#开发者工具" class="headerlink" title="开发者工具"></a>开发者工具</h3><p>在 <a target="_blank" rel="noopener" href="https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en">Chrome</a> 或者 <a target="_blank" rel="noopener" href="https://addons.mozilla.org/en-US/firefox/addon/react-devtools/">Firefox</a> 中安装扩展 React Devtools 可以让你在浏览器开发者工具中查看 React 的组件树。</p>
<p><a target="_blank" rel="noopener" href="https://react.docschina.org/static/878d91461c78d8f238e116477dfe0b46/e45a9/devtools.png"><img src="https://react.docschina.org/static/878d91461c78d8f238e116477dfe0b46/e45a9/devtools.png" class="lazyload" data-srcset="https://react.docschina.org/static/878d91461c78d8f238e116477dfe0b46/e45a9/devtools.png" srcset="" alt="React Devtools"></a></p>
<p>你还可以在 React DevTools 中检查 React 组件的 state 和 props。</p>
<h3 id="状态提升"><a href="#状态提升" class="headerlink" title="状态提升"></a>状态提升</h3><p>当前，每个 Square 组件都维护了游戏的状态。我们可以把所有 9 个 Square 的值放在一个地方，这样我们就可以判断出胜者了。</p>
<p>你可能会想，我们也可以在棋盘 Board 组件中收集每个格子 Square 组件中的 state。</p>
<p>虽然技术上来讲是可以实现的，但是代码如此编写会让人很难理解，并且我们以后想要维护重构时也会非常困难。</p>
<p>所以，最好的解决方式是直接将所有的 state 状态数据存储在 Board 父组件当中。</p>
<p>之后 Board 组件可以将这些数据通过 props 传递给各个 Square 子组件，<a target="_blank" rel="noopener" href="https://react.docschina.org/tutorial/tutorial.html#passing-data-through-props">正如上文我们把数字传递给每一个 Square 一样</a>。</p>
<p>当你遇到需要同时获取多个子组件数据，或者两个组件之间需要相互通讯的情况时，需要把子组件的 state 数据提升至其共同的父组件当中保存。之后父组件可以通过 props 将状态数据传递到子组件当中。这样应用当中所有组件的状态数据就能够更方便地同步共享了。</p>
<p>像这种将组件的 state 提升到父组件的情形在重构 React 组件时经常会遇到 —— 借此我们来实践一下。</p>
<p>为 Board 组件添加构造函数，将 Board 组件的初始状态设置为长度为 9 的空值数组：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">class Board extends React.Component &#123;</span><br><span class="line">  constructor(props) &#123;    </span><br><span class="line">      super(props);    </span><br><span class="line">      this.state = &#123;      </span><br><span class="line">          squares: Array(9).fill(null),    </span><br><span class="line">      &#125;;  </span><br><span class="line">  &#125;</span><br><span class="line">  renderSquare(i) &#123;</span><br><span class="line">    return &lt;Square value=&#123;i&#125; /&gt;;</span><br><span class="line">  &#125;</span><br></pre></td></tr></table></figure>

<p>当我们填充棋盘后，<code>this.state.squares</code> 数组的值可能如下所示：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">[</span><br><span class="line">  &#x27;O&#x27;, null, &#x27;X&#x27;,</span><br><span class="line">  &#x27;X&#x27;, &#x27;X&#x27;, &#x27;O&#x27;,</span><br><span class="line">  &#x27;O&#x27;, null, null,</span><br><span class="line">]</span><br></pre></td></tr></table></figure>

<p>Board 组件当前的 <code>renderSquare</code> 方法看起来像下面这样：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">renderSquare(i) &#123;</span><br><span class="line">  return &lt;Square value=&#123;i&#125; /&gt;;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>开始时，我们依次使把 0 到 8 的值通过 prop 从 Board <a target="_blank" rel="noopener" href="https://react.docschina.org/tutorial/tutorial.html#passing-data-through-props">向下传递</a>，从而让它们显示出来。上一步与此不同，我们<a target="_blank" rel="noopener" href="https://react.docschina.org/tutorial/tutorial.html#making-an-interactive-component">根据 Square 自己内部的 state</a>，使用了 “X” 来代替之前的数字。因此，Square 忽略了当前从 Board 传递给它的那个 <code>value</code> prop。</p>
<p>让我们再一次使用 prop 的传递机制。我们通过修改 Board 来指示每一个 Square 的当前值（<code>&#39;X&#39;</code>, <code>&#39;O&#39;</code>, 或者 <code>null</code>）。我们在 Board 的构造函数中已经定义好了 <code>squares</code> 数组，这样，我们就可以通过修改 Board 的 <code>renderSquare</code> 方法来读取这些值了。</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">renderSquare(i) &#123;</span><br><span class="line">  return &lt;Square value=&#123;this.state.squares[i]&#125; /&gt;;  &#125;</span><br></pre></td></tr></table></figure>

<p><strong><a target="_blank" rel="noopener" href="https://codepen.io/gaearon/pen/gWWQPY?editors=0010">查看此步完整代码示例</a></strong></p>
<p>这样，每个 Square 就都能接收到一个 <code>value</code> prop 了，这个 prop 的值可以是 <code>&#39;X&#39;</code>、 <code>&#39;O&#39;</code>、 或 <code>null</code>（<code>null</code> 代表空方格）。</p>
<p>接下来，我们要修改一下 Square 的点击事件监听函数。Board 组件当前维护了那些已经被填充了的方格。我们需要想办法让 Square 去更新 Board 的 state。由于 state 对于每个组件来说是私有的，因此我们不能直接通过 Square 来更新 Board 的 state。</p>
<p>相反，从 Board 组件向 Square 组件传递一个函数，当 Square 被点击的时候，这个函数就会被调用。接着，我们将 Board 组件的 <code>renderSquare</code> 方法改写为如下效果：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">renderSquare(i) &#123;</span><br><span class="line">  return (</span><br><span class="line">    &lt;Square</span><br><span class="line">      value=&#123;this.state.squares[i]&#125;</span><br><span class="line">      onClick=&#123;() =&gt; this.handleClick(i)&#125;      /&gt;</span><br><span class="line">  );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<blockquote>
<p>注意</p>
<p>为了提高可读性，我们把返回的 React 元素拆分成了多行，同时在最外层加了小括号，这样 JavaScript 解析的时候就不会在 <code>return</code> 的后面自动插入一个分号从而破坏代码结构了。</p>
</blockquote>
<p>现在我们从 Board 组件向 Square 组件中传递两个 props 参数：<code>value</code> 和 <code>onClick</code>。<code>onClick</code> prop 是一个 Square 组件点击事件监听函数。接下来，我们需要修改 Square 的代码：</p>
<ul>
<li>将 Square 组件的 <code>render</code> 方法中的 <code>this.state.value</code> 替换为 <code>this.props.value</code> 。</li>
<li>将 Square 组件的 <code>render</code> 方法中的 <code>this.setState()</code> 替换为 <code>this.props.onClick()</code> 。</li>
<li>删掉 Square 组件中的构造函数 <code>constructor</code>，因为该组件不需要再保存游戏的 state。</li>
</ul>
<p>进行上述修改之后，代码会变成下面这样:</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">class Square extends React.Component &#123;  </span><br><span class="line">    render() &#123;    </span><br><span class="line">      return (</span><br><span class="line">      &lt;button</span><br><span class="line">        className=&quot;square&quot;</span><br><span class="line">        onClick=&#123;() =&gt; this.props.onClick()&#125;      </span><br><span class="line">       &gt;</span><br><span class="line">        &#123;this.props.value&#125;      </span><br><span class="line">       &lt;/button&gt;</span><br><span class="line">    );</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>每一个 Square 被点击时，Board 提供的 <code>onClick</code> 函数就会触发。我们回顾一下这是怎么实现的：</p>
<ol>
<li>向 DOM 内置元素 <code>&lt;button&gt;</code> 添加 <code>onClick</code> prop，让 React 开启对点击事件的监听。</li>
<li>当 button 被点击时，React 会调用 Square 组件的 <code>render()</code> 方法中的 <code>onClick</code> 事件处理函数。</li>
<li>事件处理函数触发了传入其中的 <code>this.props.onClick()</code> 方法。这个方法是由 Board 传递给 Square 的。</li>
<li>由于 Board 把 <code>onClick=&#123;() =&gt; this.handleClick(i)&#125;</code> 传递给了 Square，所以当 Square 中的事件处理函数触发时，其实就是触发的 Board 当中的 <code>this.handleClick(i)</code> 方法。</li>
<li>现在我们还尚未定义 <code>handleClick()</code> 方法，所以代码还不能正常工作。如果此时点击 Square，你会在屏幕上看到红色的错误提示，提示内容为：“this.handleClick is not a function”。</li>
</ol>
<blockquote>
<p>注意</p>
<p>因为 DOM 元素 <code>&lt;button&gt;</code> 是一个内置组件，因此其 <code>onClick</code> 属性在 React 中有特殊的含义。而对于用户自定义的组件来说，命名就可以由用户自己来定义了。我们给 Square 的 <code>onClick</code> 和 Board 的 <code>handleClick</code> 赋予任意的名称，代码依旧有效。在 React 中，有一个命名规范，通常会将代表事件的监听 prop 命名为 <code>on[Event]</code>，将处理事件的监听方法命名为 <code>handle[Event]</code> 这样的格式。</p>
</blockquote>
<p>这时候我们点击 Square 的时候，浏览器会报错，因为我们还没有定义 <code>handleClick</code> 方法。我们现在来向 Board 里添加 <code>handleClick</code> 方法：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br></pre></td><td class="code"><pre><span class="line">class Board extends React.Component &#123;</span><br><span class="line">  constructor(props) &#123;</span><br><span class="line">    super(props);</span><br><span class="line">    this.state = &#123;</span><br><span class="line">      squares: Array(9).fill(null),</span><br><span class="line">    &#125;;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  handleClick(i) &#123;    </span><br><span class="line">      const squares = this.state.squares.slice();    </span><br><span class="line">      squares[i] = &#x27;X&#x27;;    </span><br><span class="line">      this.setState(&#123;squares: squares&#125;);  </span><br><span class="line">  &#125;</span><br><span class="line">  renderSquare(i) &#123;</span><br><span class="line">    return (</span><br><span class="line">      &lt;Square</span><br><span class="line">        value=&#123;this.state.squares[i]&#125;</span><br><span class="line">        onClick=&#123;() =&gt; this.handleClick(i)&#125;</span><br><span class="line">      /&gt;</span><br><span class="line">    );</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  render() &#123;</span><br><span class="line">    const status = &#x27;Next player: X&#x27;;</span><br><span class="line"></span><br><span class="line">    return (</span><br><span class="line">      &lt;div&gt;</span><br><span class="line">        &lt;div className=&quot;status&quot;&gt;&#123;status&#125;&lt;/div&gt;</span><br><span class="line">        &lt;div className=&quot;board-row&quot;&gt;</span><br><span class="line">          &#123;this.renderSquare(0)&#125;</span><br><span class="line">          &#123;this.renderSquare(1)&#125;</span><br><span class="line">          &#123;this.renderSquare(2)&#125;</span><br><span class="line">        &lt;/div&gt;</span><br><span class="line">        &lt;div className=&quot;board-row&quot;&gt;</span><br><span class="line">          &#123;this.renderSquare(3)&#125;</span><br><span class="line">          &#123;this.renderSquare(4)&#125;</span><br><span class="line">          &#123;this.renderSquare(5)&#125;</span><br><span class="line">        &lt;/div&gt;</span><br><span class="line">        &lt;div className=&quot;board-row&quot;&gt;</span><br><span class="line">          &#123;this.renderSquare(6)&#125;</span><br><span class="line">          &#123;this.renderSquare(7)&#125;</span><br><span class="line">          &#123;this.renderSquare(8)&#125;</span><br><span class="line">        &lt;/div&gt;</span><br><span class="line">      &lt;/div&gt;</span><br><span class="line">    );</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p><strong><a target="_blank" rel="noopener" href="https://codepen.io/gaearon/pen/ybbQJX?editors=0010">查看此步完整代码示例</a></strong></p>
<p>现在，我们可以通过点击 Square 来填充那些方格，效果与之前相同。但是，当前 state 没有保存在单个的 Square 组件中，而是保存在了 Board 组件中。每当 Board 的 state 发生变化的时候，这些 Square 组件都会重新渲染一次。把所有 Square 的 state 保存在 Board 组件中可以让我们在将来判断出游戏的胜者。</p>
<p>因为 Square 组件不再持有 state，因此每次它们被点击的时候，Square 组件就会从 Board 组件中接收值，并且通知 Board 组件。在 React 术语中，我们把目前的 Square 组件称做“受控组件”。在这种情况下，Board 组件完全控制了 Square 组件。</p>
<p>注意，我们调用了 <code>.slice()</code> 方法创建了 <code>squares</code> 数组的一个副本，而不是直接在现有的数组上进行修改。在下一节，我们会介绍为什么我们需要创建 <code>square</code> 数组的副本。</p>
<h3 id="为什么不可变性在-React-中非常重要"><a href="#为什么不可变性在-React-中非常重要" class="headerlink" title="为什么不可变性在 React 中非常重要"></a>为什么不可变性在 React 中非常重要</h3><p>在上一节内容当中，我们通过使用 <code>.slice()</code> 方法创建了数组的一个副本，而不是直接修改现有的数组。接下来我们来学习不可变性以及不可变性的重要性。</p>
<p>一般来说，有两种改变数据的方式。第一种方式是直接<em>修改</em>变量的值，第二种方式是使用新的一份数据替换旧数据。</p>
<h4 id="-1"><a href="#-1" class="headerlink" title=""></a></h4><h3 id="函数组件"><a href="#函数组件" class="headerlink" title="函数组件"></a>函数组件</h3><p>接下来我们把 Square 组件重写为一个<strong>函数组件</strong>。</p>
<p>如果你想写的组件只包含一个 <code>render</code> 方法，并且不包含 state，那么使用<strong>函数组件</strong>就会更简单。我们不需要定义一个继承于 <code>React.Component</code> 的类，我们可以定义一个函数，这个函数接收 <code>props</code> 作为参数，然后返回需要渲染的元素。函数组件写起来并不像 class 组件那么繁琐，很多组件都可以使用函数组件来写。</p>
<p>把 Square 类替换成下面的函数：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">class Square extends React.Component &#123;</span><br><span class="line">    constructor(props) &#123;</span><br><span class="line">        super(props);</span><br><span class="line">        this.state = &#123;</span><br><span class="line">            value: null,</span><br><span class="line">        &#125;;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    render() &#123;</span><br><span class="line">      return (</span><br><span class="line">        &lt;button </span><br><span class="line">            className=&quot;square&quot; </span><br><span class="line">            onClick=&#123;() =&gt; this.props.onClick() &#125;</span><br><span class="line">        &gt;</span><br><span class="line">          &#123;this.props.value&#125;</span><br><span class="line">        &lt;/button&gt;</span><br><span class="line">      );</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br></pre></td></tr></table></figure>

<p>替换成</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">function Square(props) &#123;</span><br><span class="line">  return (</span><br><span class="line">    &lt;button className=&quot;square&quot; onClick=&#123;props.onClick&#125;&gt;</span><br><span class="line">      &#123;props.value&#125;</span><br><span class="line">    &lt;/button&gt;</span><br><span class="line">  );</span><br></pre></td></tr></table></figure>

<p>我们把两个 <code>this.props</code> 都替换成了 <code>props</code>。</p>
<p><strong><a target="_blank" rel="noopener" href="https://codepen.io/gaearon/pen/QvvJOv?editors=0010">查看此步完整代码示例</a></strong></p>
<blockquote>
<p>注意</p>
<p>当我们把 Square 修改成函数组件时，我们同时也把 <code>onClick=&#123;() =&gt; this.props.onClick()&#125;</code> 改成了更短的 <code>onClick=&#123;props.onClick&#125;</code>（注意两侧<em>都</em>没有括号）。</p>
</blockquote>
<h3 id="轮流落子"><a href="#轮流落子" class="headerlink" title="轮流落子"></a>轮流落子</h3><p>现在井字棋还有一个明显的缺陷有待完善：目前还不能在棋盘上标记 “O”。</p>
<p>我们将 “X” 默认设置为先手棋。你可以通过修改 Board 组件的构造函数中的初始 state 来设置默认的第一步棋子：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">class Board extends React.Component &#123;</span><br><span class="line">  constructor(props) &#123;</span><br><span class="line">    super(props);</span><br><span class="line">    this.state = &#123;</span><br><span class="line">      squares: Array(9).fill(null),</span><br><span class="line">      xIsNext: true,    </span><br><span class="line">    &#125;;</span><br><span class="line">  &#125;</span><br></pre></td></tr></table></figure>

<p>棋子每移动一步，<code>xIsNext</code>（布尔值）都会反转，该值将确定下一步轮到哪个玩家，并且游戏的状态会被保存下来。我们将通过修改 Board 组件的 <code>handleClick</code> 函数来反转 <code>xIsNext</code> 的值：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">handleClick(i) &#123;</span><br><span class="line">  const squares = this.state.squares.slice();</span><br><span class="line">  squares[i] = this.state.xIsNext ? &#x27;X&#x27; : &#x27;O&#x27;;    </span><br><span class="line">    this.setState(&#123;</span><br><span class="line">    squares: squares,</span><br><span class="line">    xIsNext: !this.state.xIsNext,    </span><br><span class="line">    &#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>修改之后，我们就实现了 “X” 和 “O” 轮流落子的效果。尝试玩一下。</p>
<p>接下来修改 Board 组件 <code>render</code> 方法中 “status” 的值，这样就可以显示下一步是哪个玩家的了。</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">render() &#123;</span><br><span class="line">  const status = &#x27;Next player: &#x27; + (this.state.xIsNext ? &#x27;X&#x27; : &#x27;O&#x27;);</span><br><span class="line">  return (</span><br><span class="line">    // 其他部分没有改变</span><br></pre></td></tr></table></figure>

<p>现在你整个的 Board 组件的代码应该是下面这样的：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br></pre></td><td class="code"><pre><span class="line">class Board extends React.Component &#123;</span><br><span class="line">  constructor(props) &#123;</span><br><span class="line">    super(props);</span><br><span class="line">    this.state = &#123;</span><br><span class="line">      squares: Array(9).fill(null),</span><br><span class="line">      xIsNext: true,    &#125;;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  handleClick(i) &#123;</span><br><span class="line">    const squares = this.state.squares.slice();    </span><br><span class="line">    squares[i] = this.state.xIsNext ? &#x27;X&#x27; : &#x27;O&#x27;;</span><br><span class="line">    this.setState(&#123;      </span><br><span class="line">        squares: squares,      </span><br><span class="line">        xIsNext: !this.state.xIsNext,    </span><br><span class="line">    &#125;);  </span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  renderSquare(i) &#123;</span><br><span class="line">    return (</span><br><span class="line">      &lt;Square</span><br><span class="line">        value=&#123;this.state.squares[i]&#125;</span><br><span class="line">        onClick=&#123;() =&gt; this.handleClick(i)&#125;</span><br><span class="line">      /&gt;</span><br><span class="line">    );</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  render() &#123;</span><br><span class="line">    const status = &#x27;Next player: &#x27; + (this.state.xIsNext ? &#x27;X&#x27; : &#x27;O&#x27;);</span><br><span class="line">    return (</span><br><span class="line">      &lt;div&gt;</span><br><span class="line">        &lt;div className=&quot;status&quot;&gt;&#123;status&#125;&lt;/div&gt;</span><br><span class="line">        &lt;div className=&quot;board-row&quot;&gt;</span><br><span class="line">          &#123;this.renderSquare(0)&#125;</span><br><span class="line">          &#123;this.renderSquare(1)&#125;</span><br><span class="line">          &#123;this.renderSquare(2)&#125;</span><br><span class="line">        &lt;/div&gt;</span><br><span class="line">        &lt;div className=&quot;board-row&quot;&gt;</span><br><span class="line">          &#123;this.renderSquare(3)&#125;</span><br><span class="line">          &#123;this.renderSquare(4)&#125;</span><br><span class="line">          &#123;this.renderSquare(5)&#125;</span><br><span class="line">        &lt;/div&gt;</span><br><span class="line">        &lt;div className=&quot;board-row&quot;&gt;</span><br><span class="line">          &#123;this.renderSquare(6)&#125;</span><br><span class="line">          &#123;this.renderSquare(7)&#125;</span><br><span class="line">          &#123;this.renderSquare(8)&#125;</span><br><span class="line">        &lt;/div&gt;</span><br><span class="line">      &lt;/div&gt;</span><br><span class="line">    );</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p><strong><a target="_blank" rel="noopener" href="https://codepen.io/gaearon/pen/KmmrBy?editors=0010">查看此步完整代码示例</a></strong></p>
<h3 id="判断出胜者"><a href="#判断出胜者" class="headerlink" title="判断出胜者"></a>判断出胜者</h3><p>至此我们就可以看出下一步会轮到哪位玩家，与此同时，我们还需要显示游戏的结果来判定游戏结束。拷贝如下 calculateWinner 函数并粘贴到文件底部：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">function calculateWinner(squares) &#123;</span><br><span class="line">  const lines = [</span><br><span class="line">    [0, 1, 2],</span><br><span class="line">    [3, 4, 5],</span><br><span class="line">    [6, 7, 8],</span><br><span class="line">    [0, 3, 6],</span><br><span class="line">    [1, 4, 7],</span><br><span class="line">    [2, 5, 8],</span><br><span class="line">    [0, 4, 8],</span><br><span class="line">    [2, 4, 6],</span><br><span class="line">  ];</span><br><span class="line">  for (let i = 0; i &lt; lines.length; i++) &#123;</span><br><span class="line">    const [a, b, c] = lines[i];</span><br><span class="line">    if (squares[a] &amp;&amp; squares[a] === squares[b] &amp;&amp; squares[a] === squares[c]) &#123;</span><br><span class="line">      return squares[a];</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  return null;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>传入长度为 9 的数组，此函数将判断出获胜者，并根据情况返回 “X”，“O” 或 “null”。</p>
<p>接着，在 Board 组件的 <code>render</code> 方法中调用 <code>calculateWinner(squares)</code> 检查是否有玩家胜出。一旦有一方玩家胜出，就把获胜玩家的信息显示出来，比如，“胜者：X” 或者“胜者：O”。现在，我们把 Board 的 <code>render</code> 函数中的 status 的定义修改为如下代码：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">render() &#123;</span><br><span class="line">  const winner = calculateWinner(this.state.squares);    </span><br><span class="line">  let status;    </span><br><span class="line">    if (winner) &#123;      </span><br><span class="line">        status = &#x27;Winner: &#x27; + winner;    </span><br><span class="line">    &#125; else &#123;      </span><br><span class="line">        status = &#x27;Next player: &#x27; + (this.state.xIsNext ? &#x27;X&#x27; : &#x27;O&#x27;);    </span><br><span class="line">    &#125;</span><br><span class="line">  return (</span><br><span class="line">    // 其他部分没有修改</span><br></pre></td></tr></table></figure>

<p>最后，修改 <code>handleClick</code> 事件，当有玩家胜出时，或者某个 Square 已经被填充时，该函数不做任何处理直接返回。</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">handleClick(i) &#123;</span><br><span class="line">  const squares = this.state.squares.slice();</span><br><span class="line">  if (calculateWinner(squares) || squares[i]) &#123;      </span><br><span class="line">      return;    </span><br><span class="line">  &#125;    </span><br><span class="line">  squares[i] = this.state.xIsNext ? &#x27;X&#x27; : &#x27;O&#x27;;</span><br><span class="line">  this.setState(&#123;</span><br><span class="line">    squares: squares,</span><br><span class="line">    xIsNext: !this.state.xIsNext,</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p><strong><a target="_blank" rel="noopener" href="https://codepen.io/gaearon/pen/LyyXgK?editors=0010">查看此步完整代码示例</a></strong></p>
<p>恭喜！现在你已经完成了井字棋！除此之外，你也已经掌握了 React 的基本常识。所以坚持到这一步的你才是真正的赢家呀！</p>

  
  
  
  

  
    <div class="prev-next">
      
        <a class='prev' href='/blog/computernet.html'>
          <p class='title'><i class="fas fa-chevron-left" aria-hidden="true"></i>计算机网络简答题</p>
          <p class='content'>一写出多个应用层的协议及对应的端口号，以及再传输层交给TCP还是UDP


协议动态主机配置协议
端口号
传输层



动态主机配置协议 DHCP
端口号68
(UDP)


域名系统DNS
端...</p>
        </a>
      
      
        <a class='next' href='/blog/javaScript.html'>
          <p class='title'>ES6新特性箭头函数<i class="fas fa-chevron-right" aria-hidden="true"></i></p>
          <p class='content'>ES6新特性箭头函数ES6标准新增了一种新的函数：Arrow Function（箭头函数）。
为什么叫Arrow Function？因为它的定义用的就是一个箭头：
1x =&gt; x * x
...</p>
        </a>
      
    </div>
  
</article>


  

  <article class="post white-box reveal shadow" id="comments">
    <p ct><i class='fas fa-comments'></i> 评论</p>
    
    <div id="valine_container" class="valine_thread">
  <i class="fas fa-cog fa-spin fa-fw fa-2x"></i>
</div>

  </article>






</div>
<aside class='l_side'>
  
  
    
    



  <section class="widget toc-wrapper shadow desktop mobile" id="toc-div" >
    
  <header>
    
      <i class="fas fa-list fa-fw" aria-hidden="true"></i><span class='name'>本文目录</span>
    
  </header>


    <div class='content'>
        <ol class="toc"><li class="toc-item toc-level-2"><a class="toc-link" href="#React-%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F"><span class="toc-text">React 是什么？</span></a><ol class="toc-child"><li class="toc-item toc-level-3"><a class="toc-link" href="#JXS"><span class="toc-text">JXS</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E9%80%9A%E8%BF%87-Props-%E4%BC%A0%E9%80%92%E6%95%B0%E6%8D%AE"><span class="toc-text">通过 Props 传递数据</span></a></li><li class="toc-item toc-level-3"><a class="toc-link"><span class="toc-text"></span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E7%BB%99%E7%BB%84%E4%BB%B6%E6%B7%BB%E5%8A%A0%E4%BA%A4%E4%BA%92%E5%8A%9F%E8%83%BD"><span class="toc-text">给组件添加交互功能</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E5%BC%80%E5%8F%91%E8%80%85%E5%B7%A5%E5%85%B7"><span class="toc-text">开发者工具</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E7%8A%B6%E6%80%81%E6%8F%90%E5%8D%87"><span class="toc-text">状态提升</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%8D%E5%8F%AF%E5%8F%98%E6%80%A7%E5%9C%A8-React-%E4%B8%AD%E9%9D%9E%E5%B8%B8%E9%87%8D%E8%A6%81"><span class="toc-text">为什么不可变性在 React 中非常重要</span></a><ol class="toc-child"><li class="toc-item toc-level-4"><a class="toc-link" href="#-1"><span class="toc-text"></span></a></li></ol></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E5%87%BD%E6%95%B0%E7%BB%84%E4%BB%B6"><span class="toc-text">函数组件</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E8%BD%AE%E6%B5%81%E8%90%BD%E5%AD%90"><span class="toc-text">轮流落子</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E5%88%A4%E6%96%AD%E5%87%BA%E8%83%9C%E8%80%85"><span class="toc-text">判断出胜者</span></a></li></ol></li></ol>
    </div>
  </section>


  


</aside>



		  
		  <!--此文件用来存放一些不方便取值的变量-->
<!--思路大概是将值藏到重加载的区域内-->

<script>
  window.pdata={}
  pdata.ispage=false;
  pdata.postTitle="";
  pdata.commentPath="";
  pdata.commentPlaceholder="";
  // header 这里无论是否开启pjax都需要
  var l_header=document.getElementById("l_header");
  
  l_header.classList.add("show");
  
  
    // cover
    var cover_wrapper=document.querySelector('.cover-wrapper');
    
    cover_wrapper.id="none";
    cover_wrapper.style.display="none";
    
  
</script>

        </div>
        
  
  <footer class="footer clearfix">
    <br><br>
    
      
        <div class="aplayer-container">
          


        </div>
      
    
      
        <br>
        <div class="social-wrapper">
          
            
          
            
          
            
          
        </div>
      
    
      
        <div><p>博客内容遵循 <a target="_blank" rel="noopener" href="https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh">署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 协议</a></p>
</div>
      
    
      
        
          <div><p><span id="lc-sv">本站总访问量为 <span id='number'><i class="fas fa-circle-notch fa-spin fa-fw" aria-hidden="true"></i></span> 次</span> <span id="lc-uv">访客数为 <span id='number'><i class="fas fa-circle-notch fa-spin fa-fw" aria-hidden="true"></i></span> 人</span></p>
</div>
        
      
    
      
        本站使用
        <a href="https://github.com/volantis-x/hexo-theme-volantis/tree/4.3.1" target="_blank" class="codename">Volantis</a>
        作为主题
      
    
      
        <div class='copyright'>
        <p><a href="/">Copyright © 2017-2020 XXX</a></p>

        </div>
      
    
  </footer>


        <a id="s-top" class="fas fa-arrow-up fa-fw" href="javascript:void(0)"></a>
      </div>
    </div>
    <div>
      <script>
/************这个文件存放不需要重载的全局变量和全局函数*********/
window.volantis={};
window.volantis.loadcss=document.getElementById("loadcss");
/******************** Pjax ********************************/
function VPjax(){
	this.list=[] // 存放回调函数
	this.start=()=>{
	  for(var i=0;i<this.list.length;i++){
		this.list[i].run();
	  }
	}
	this.push=(fn,name)=>{
		var f=new PjaxItem(fn,name);
		this.list.push(f);
	}
	// 构造一个可以run的对象
	function PjaxItem(fn,name){
		// 函数名称
		this.name = name || fn.name
		// run方法
		this.run=()=>{
			fn()
		}
	}
}
volantis.pjax={}
volantis.pjax.method={
	complete: new VPjax(),
	error: new VPjax(),
	send: new VPjax()
}
volantis.pjax={
	...volantis.pjax,
	push: volantis.pjax.method.complete.push,
	error: volantis.pjax.method.error.push,
	send: volantis.pjax.method.send.push
}
/********************脚本懒加载函数********************************/
// 已经加入了setTimeout
function loadScript(src, cb) {
	setTimeout(function() {
		var HEAD = document.getElementsByTagName('head')[0] || document.documentElement;
		var script = document.createElement('script');
		script.setAttribute('type','text/javascript');
		if (cb) script.onload = cb;
		script.setAttribute('src', src);
		HEAD.appendChild(script);
	});
}
//https://github.com/filamentgroup/loadCSS
var loadCSS = function( href, before, media, attributes ){
	var doc = window.document;
	var ss = doc.createElement( "link" );
	var ref;
	if( before ){
		ref = before;
	}
	else {
		var refs = ( doc.body || doc.getElementsByTagName( "head" )[ 0 ] ).childNodes;
		ref = refs[ refs.length - 1];
	}
	var sheets = doc.styleSheets;
	if( attributes ){
		for( var attributeName in attributes ){
			if( attributes.hasOwnProperty( attributeName ) ){
				ss.setAttribute( attributeName, attributes[attributeName] );
			}
		}
	}
	ss.rel = "stylesheet";
	ss.href = href;
	ss.media = "only x";
	function ready( cb ){
		if( doc.body ){
			return cb();
		}
		setTimeout(function(){
			ready( cb );
		});
	}
	ready( function(){
		ref.parentNode.insertBefore( ss, ( before ? ref : ref.nextSibling ) );
	});
	var onloadcssdefined = function( cb ){
		var resolvedHref = ss.href;
		var i = sheets.length;
		while( i-- ){
			if( sheets[ i ].href === resolvedHref ){
				return cb();
			}
		}
		setTimeout(function() {
			onloadcssdefined( cb );
		});
	};
	function loadCB(){
		if( ss.addEventListener ){
			ss.removeEventListener( "load", loadCB );
		}
		ss.media = media || "all";
	}
	if( ss.addEventListener ){
		ss.addEventListener( "load", loadCB);
	}
	ss.onloadcssdefined = onloadcssdefined;
	onloadcssdefined( loadCB );
	return ss;
};
</script>
<script>
  
  loadCSS("https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@5.14/css/all.min.css", window.volantis.loadcss);
  
  
  
  
</script>
<!-- required -->

<script src="https://cdn.jsdelivr.net/npm/jquery@3.5/dist/jquery.min.js"></script>

<script>
  function pjax_fancybox() {
    $(".md .gallery").find("img").each(function () { //渲染 fancybox
      var element = document.createElement("a"); // a 标签
      $(element).attr("class", "fancybox");
      $(element).attr("pjax-fancybox", "");  // 过滤 pjax
      $(element).attr("href", $(this).attr("src"));
      if ($(this).attr("data-original")) {
        $(element).attr("href", $(this).attr("data-original"));
      }
      $(element).attr("data-fancybox", "images");
      var caption = "";   // 描述信息
      if ($(this).attr('alt')) {  // 判断当前页面是否存在描述信息
        $(element).attr('data-caption', $(this).attr('alt'));
        caption = $(this).attr('alt');
      }
      var div = document.createElement("div");
      $(div).addClass("fancybox");
      $(this).wrap(div); // 最外层套 div ，其实主要作用还是 class 样式
      var span = document.createElement("span");
      $(span).addClass("image-caption");
      $(span).text(caption); // 加描述
      $(this).after(span);  // 再套一层描述
      $(this).wrap(element);  // 最后套 a 标签
    })
    $(".md .gallery").find("img").fancybox({
      selector: '[data-fancybox="images"]',
      hash: false,
      loop: false,
      closeClick: true,
      helpers: {
        overlay: {closeClick: true}
      },
      buttons: [
        "zoom",
        "close"
      ]
    });
  };
  function SCload_fancybox() {
    if ($(".md .gallery").find("img").length == 0) return;
    loadCSS("https://cdn.jsdelivr.net/npm/@fancyapps/fancybox@3.5.7/dist/jquery.fancybox.min.css", document.getElementById("loadcss"));
    loadScript('https://cdn.jsdelivr.net/gh/fancyapps/fancybox@3.5.7/dist/jquery.fancybox.min.js', pjax_fancybox)
  };
  $(function () {
    SCload_fancybox();
  });
  function Pjax_SCload_fancybox(){
	if (typeof $.fancybox == "undefined") {
	 SCload_fancybox();
    } else {
	 pjax_fancybox();
    }
  }
  volantis.pjax.push(Pjax_SCload_fancybox)
  volantis.pjax.send(()=>{
      if (typeof $.fancybox != "undefined") {
        $.fancybox.close();    // 关闭弹窗
      }
  },'fancybox')
</script>


<!-- internal -->



  
  

  <div id="rightmenu-wrapper">
    <ul class="list-v rightmenu" id="rightmenu-content">
      <li class='option'>
        <a class='vlts-menu opt fix-cursor-default' id='menu-copy-text' onclick="document.execCommand('copy')"><i class='fa fa-copy fa-fw'></i>复制文本</a>
        <hr id='hr-text'>
        <a class='vlts-menu opt fix-cursor-default' id='menu-copy-href'><i class='fa fa-link fa-fw'></i>复制链接</a>
        <a class='vlts-menu opt fix-cursor-default' id='menu-open-href'><i class='fa fa-external-link-square-alt fa-fw'></i>在新标签页打开</a>
        <hr id='hr-href'>
        <a class='vlts-menu opt fix-cursor-default' id='menu-copy-src'><i class='fa fa-image fa-fw'></i>复制图片地址</a>
        <hr id='hr-src'>
      </li>
      
        <li class='navigation'>
          <a class='nav icon-only fix-cursor-default' onclick='history.back()'><i class='fa fa-arrow-left fa-fw'></i></a>
          <a class='nav icon-only fix-cursor-default' onclick='history.forward()'><i class='fa fa-arrow-right fa-fw'></i></a>
          <a class='nav icon-only fix-cursor-default' onclick='window.location.reload()'><i class='fa fa-redo fa-fw'></i></a>
          <a class='nav icon-only fix-cursor-default' href='/'><i class='fa fa-home fa-fw'></i></a>
        </li>
      
      <hr>
      
        
      
        
          <hr>
        
      
        
          
    <li>
      <a class='vlts-menu fix-cursor-default ' href=https://volantis.js.org/faqs/
        
        
        
        
          id="https:volantisjsorgfaqs"
        >
        <i class='fa fa-question fa-fw'></i> 常见问题
      </a>
    </li>
  
        
      
        
          
    <li>
      <a class='vlts-menu fix-cursor-default ' href=https://volantis.js.org/examples/
        
        
        
        
          id="https:volantisjsorgexamples"
        >
        <i class='fa fa-rss fa-fw'></i> 示例博客
      </a>
    </li>
  
        
      
        
          
    <li>
      <a class='vlts-menu fix-cursor-default ' href=https://volantis.js.org/contributors/
        
        
        
        
          id="https:volantisjsorgcontributors"
        >
        <i class='fa fa-fan fa-spin fa-fw'></i> 加入社区
      </a>
    </li>
  
        
      
        
          <hr>
        
      
        
          
    <li>
      <a class='vlts-menu fix-cursor-default ' href=https://github.com/volantis-x/volantis-docs/
        
        
        
        
          id="https:githubcomvolantis-xvolantis-docs"
        >
        <i class='fa fa-code-branch fa-fw'></i> 本站源码
      </a>
    </li>
  
        
      
        
          
    <li>
      <a class='vlts-menu fix-cursor-default ' href=https://github.com/volantis-x/hexo-theme-volantis/
        
        
        
        
          id="https:githubcomvolantis-xhexo-theme-volantis"
        >
        <i class='fa fa-code-branch fa-fw'></i> 主题源码
      </a>
    </li>
  
        
      
        
          <hr>
        
      
        
          
    <li>
      <a class='vlts-menu fix-cursor-default ' 
        
        
        
          onclick="document.execCommand('print')"
        
        >
        <i class='fa fa-print fa-fw'></i> 打印页面
      </a>
    </li>
  
        
      
        
          
    <li>
      <a class='vlts-menu fix-cursor-default ' 
        
        
        
        >
        <i class='fa fa-moon fa-fw'></i> 暗黑模式
      </a>
    </li>
  
        
      
        
      
    </ul>
  </div>

  <script>
    window.document.oncontextmenu = function (event) {
      if (event.ctrlKey) return true;
      if (/Android|webOS|BlackBerry/i.test(navigator.userAgent)) return true;
      return popMenu(event);
    };
    document.addEventListener("click", function (event) {
      var mymenu = document.getElementById('rightmenu-wrapper');
      mymenu.style.display = "none";
    });
    function popMenu(event) {
      var mymenu = document.getElementById('rightmenu-wrapper');
      var menuContent = document.getElementById('rightmenu-content');
      var screenWidth = document.documentElement.clientWidth || document.body.clientWidth;
      var screenHeight = document.documentElement.clientHeight || document.body.clientHeight;
      mymenu.style.left = event.clientX + "px"; // 获取鼠标位置
      mymenu.style.top = event.clientY + "px";
      mymenu.style.display = 'block';
      if (event.clientX * 2 > screenWidth) {
        menuContent.classList.add('left');
      } else {
        menuContent.classList.remove('left');
      }
      if (event.clientY * 2 > screenHeight) {
        menuContent.classList.add('top');
      } else {
        menuContent.classList.remove('top');
      }

      let hrText = document.getElementById('hr-text');
      let hrSrc = document.getElementById('hr-src');
      let hrHref = document.getElementById('hr-href');

      // 选中图片
      let copySrc = document.getElementById('menu-copy-src');
      if (copySrc != undefined) {
        if (event.target.currentSrc) {
          copySrc.style.display = 'block';
          copySrc.addEventListener("click", function (e) {
            copyString(event.target.currentSrc);
          },{once: true});
          hrSrc.style.display = 'block';
        } else {
          copySrc.style.display = 'none';
          hrSrc.style.display = 'none';
        }
      }

      // 选中按钮
      // 判断是不是按钮
      let href = '';
      if (event.path) {
        for (i = 0; i < event.path.length; i++) {
          if (event.path[i].href != undefined && event.path[i].href.length > 0) {
            href = event.path[i].href;
          }
        }
      }

      let copyText = document.getElementById('menu-copy-text');
      copyText.style.display = 'none';
      hrText.style.display = 'none';
      if (href.length == 0) {
        // 选中文本
        if (window.getSelection().toString()) {
          if ('' == 'true') {
            copyText.style.display = 'block';
            hrText.style.display = 'block';
          }
        }
      }

      // 在新标签页打开
      let openHref = document.getElementById('menu-open-href');
      if (openHref != undefined) {
        if (href.length > 0) {
          openHref.style.display = 'block';
          openHref.addEventListener("click", function (e) {
            window.open(href);
          },{once: true});
          hrHref.style.display = 'block';
        } else {
          openHref.style.display = 'none';
          hrHref.style.display = 'none';
        }
      }
      // 复制链接
      let copyHref = document.getElementById('menu-copy-href');
      if (copyHref != undefined) {
        if (href.length > 0) {
          copyHref.style.display = 'block';
          copyHref.addEventListener("click", function (e) {
            copyString(href);
          },{once: true});
        } else {
          copyHref.style.display = 'none';
        }
      }
      // 有音乐播放器 see: layout/_third-party/aplayer/script.ejs
      
      return false; // 该行代码确保系统自带的右键菜单不会被调出
    }
    function hideMenu() {
      document.getElementById('rightmenu-wrapper').style.display = 'none';
    }
    function copyString(str) {
      const input = document.createElement('input');
      input.setAttribute('readonly', 'readonly');
      document.body.appendChild(input);
      input.setAttribute('value', str);
      input.select();
      document.execCommand('copy');
      document.body.removeChild(input);
    }
    document.execCommand('click');
  </script>




<script>
  function loadIssuesJS() {
    if ($(".md").find(".issues-api").length == 0) return;
	
	  loadScript('/blog/js/issues.js');
	
  };
  $(function () {
    loadIssuesJS();
  });
  volantis.pjax.push(()=>{
	if (typeof IssuesAPI == "undefined") {
	  loadIssuesJS();
	}
  },"IssuesJS")
</script>



  <script defer src="https://cdn.jsdelivr.net/npm/vanilla-lazyload@17.1.0/dist/lazyload.min.js"></script>
<script>
  // https://www.npmjs.com/package/vanilla-lazyload
  // Set the options globally
  // to make LazyLoad self-initialize
  window.lazyLoadOptions = {
    elements_selector: ".lazyload",
    threshold: 0
  };
  // Listen to the initialization event
  // and get the instance of LazyLoad
  window.addEventListener(
    "LazyLoad::Initialized",
    function (event) {
      window.lazyLoadInstance = event.detail.instance;
    },
    false
  );
  document.addEventListener('DOMContentLoaded', function () {
    lazyLoadInstance.update();
  });
  document.addEventListener('pjax:complete', function () {
    lazyLoadInstance.update();
  });
</script>




  

<script>
  window.FPConfig = {
	delay: 0,
	ignoreKeywords: [],
	maxRPS: 5,
	hoverDelay: 25
  };
</script>
<script defer src="https://cdn.jsdelivr.net/gh/gijo-varghese/flying-pages@2.1.2/flying-pages.min.js"></script>











  
  
<script src="/blog/js/valine.js"></script>


<script>
  function emoji(path, idx, ext) {
    return path + "/" + path + "-" + idx + "." + ext;
  }
  var emojiMaps = {};
  for (var i = 1; i <= 54; i++) {
    emojiMaps['tieba-' + i] = emoji('tieba', i, 'png');
  }
  for (var i = 1; i <= 101; i++) {
    emojiMaps['qq-' + i] = emoji('qq', i, 'gif');
  }
  for (var i = 1; i <= 116; i++) {
    emojiMaps['aru-' + i] = emoji('aru', i, 'gif');
  }
  for (var i = 1; i <= 125; i++) {
    emojiMaps['twemoji-' + i] = emoji('twemoji', i, 'png');
  }
  for (var i = 1; i <= 4; i++) {
    emojiMaps['weibo-' + i] = emoji('weibo', i, 'png');
  }
  function pjax_valine() {
    if(!document.querySelectorAll("#valine_container")[0])return;
    let pagePlaceholder = pdata.commentPlaceholder || "快来评论吧~";
    let path = pdata.commentPath;
    if (path.length == 0) {
      let defaultPath = '';
      path = defaultPath || decodeURI(window.location.pathname);
    }
    var valine = new Valine();
    valine.init(Object.assign({"path":null,"placeholder":"快来评论吧~","appId":null,"appKey":null,"meta":["nick","mail","link"],"requiredFields":["nick","mail"],"enableQQ":true,"recordIP":false,"avatar":"robohash","pageSize":10,"lang":"zh-cn","highlight":true,"mathJax":false}, {
      el: '#valine_container',
      path: path,
      placeholder: pagePlaceholder,
      emojiCDN: 'https://cdn.jsdelivr.net/gh/volantis-x/cdn-emoji/valine/',
      emojiMaps: emojiMaps,
    }))
  }
  $(function () {
    pjax_valine();
  });
  volantis.pjax.push(pjax_valine);
</script>






  
<script src="/blog/js/app.js"></script>



<!-- optional -->

  <script>
const SearchServiceimagePath="https://cdn.jsdelivr.net/gh/volantis-x/cdn-volantis@master/img/";
const ROOT =  ("/blog/" || "/").endsWith('/') ? ("/blog/" || "/") : ("/blog//" || "/" );

$('.input.u-search-input').one('focus',function(){
	
		loadScript('/blog/js/search/hexo.js',setSearchService);
	
})

function listenSearch(){
  
    customSearch = new HexoSearch({
      imagePath: SearchServiceimagePath
    });
  
}
function setSearchService() {
	listenSearch();
	
}
</script>











  <script defer>

  const LCCounter = {
    app_id: 'u9j57bwJod4EDmXWdxrwuqQT-MdYXbMMI',
    app_key: 'jfHtEKVE24j0IVCGHbvuFClp',
    custom_api_server: '',

    // 查询存储的记录
    getRecord(Counter, url, title) {
      return new Promise(function (resolve, reject) {
        Counter('get', '/classes/Counter?where=' + encodeURIComponent(JSON.stringify({url})))
          .then(resp => resp.json())
          .then(({results, code, error}) => {
            if (code === 401) {
              throw error;
            }
            if (results && results.length > 0) {
              var record = results[0];
              resolve(record);
            } else {
              Counter('post', '/classes/Counter', {url, title: title, times: 0})
                .then(resp => resp.json())
                .then((record, error) => {
                  if (error) {
                    throw error;
                  }
                  resolve(record);
                }).catch(error => {
                console.error('Failed to create', error);
                reject(error);
              });
            }
          }).catch((error) => {
          console.error('LeanCloud Counter Error:', error);
          reject(error);
        });
      })
    },

    // 发起自增请求
    increment(Counter, incrArr) {
      return new Promise(function (resolve, reject) {
        Counter('post', '/batch', {
          "requests": incrArr
        }).then((res) => {
          res = res.json();
          if (res.error) {
            throw res.error;
          }
          resolve(res);
        }).catch((error) => {
          console.error('Failed to save visitor count', error);
          reject(error);
        });
      });
    },

    // 构建自增请求体
    buildIncrement(objectId) {
      return {
        "method": "PUT",
        "path": `/1.1/classes/Counter/${ objectId }`,
        "body": {
          "times": {
            '__op': 'Increment',
            'amount': 1
          }
        }
      }
    },

    // 校验是否为有效的 UV
    validUV() {
      var key = 'LeanCloudUVTimestamp';
      var flag = localStorage.getItem(key);
      if (flag) {
        // 距离标记小于 24 小时则不计为 UV
        if (new Date().getTime() - parseInt(flag) <= 86400000) {
          return false;
        }
      }
      localStorage.setItem(key, new Date().getTime().toString());
      return true;
    },

    addCount(Counter) {
      var enableIncr = '' === 'true' && window.location.hostname !== 'localhost';
      enableIncr = true;
      var getterArr = [];
      var incrArr = [];
      // 请求 PV 并自增
      var pvCtn = document.querySelector('#lc-sv');
      if (pvCtn || enableIncr) {
        var pvGetter = this.getRecord(Counter, 'http://shouyeren24.gitee.io' + '/#lc-sv', 'Visits').then((record) => {
          incrArr.push(this.buildIncrement(record.objectId))
          var eles = document.querySelectorAll('#lc-sv #number');
          if (eles.length > 0) {
            eles.forEach((el,index,array)=>{
              el.innerText = record.times + 1;
              if (pvCtn) {
                pvCtn.style.display = 'inline';
              }
            })
          }
        });
        getterArr.push(pvGetter);
      }

      // 请求 UV 并自增
      var uvCtn = document.querySelector('#lc-uv');
      if (uvCtn || enableIncr) {
        var uvGetter = this.getRecord(Counter, 'http://shouyeren24.gitee.io' + '/#lc-uv', 'Visitors').then((record) => {
          var vuv = this.validUV();
          vuv && incrArr.push(this.buildIncrement(record.objectId))
          var eles = document.querySelectorAll('#lc-uv #number');
          if (eles.length > 0) {
            eles.forEach((el,index,array)=>{
              el.innerText = record.times + (vuv ? 1 : 0);
              if (uvCtn) {
                uvCtn.style.display = 'inline';
              }
            })
          }
        });
        getterArr.push(uvGetter);
      }

      // 请求文章的浏览数，如果是当前页面就自增
      var allPV = document.querySelectorAll('#lc-pv');
      if (allPV.length > 0 || enableIncr) {
        for (i = 0; i < allPV.length; i++) {
          let pv = allPV[i];
          let title = pv.getAttribute('data-title');
          var url = 'http://shouyeren24.gitee.io' + pv.getAttribute('data-path');
          if (url) {
            var viewGetter = this.getRecord(Counter, url, title).then((record) => {
              // 是当前页面就自增
              let curPath = window.location.pathname;
              if (curPath.includes('index.html')) {
                curPath = curPath.substring(0, curPath.lastIndexOf('index.html'));
              }
              if (pv.getAttribute('data-path') == curPath) {
                incrArr.push(this.buildIncrement(record.objectId));
              }
              if (pv) {
                var ele = pv.querySelector('#lc-pv #number');
                if (ele) {
                  if (pv.getAttribute('data-path') == curPath) {
                    ele.innerText = (record.times || 0) + 1;
                  } else {
                    ele.innerText = record.times || 0;
                  }
                  pv.style.display = 'inline';
                }
              }
            });
            getterArr.push(viewGetter);
          }
        }
      }

      // 如果启动计数自增，批量发起自增请求
      if (enableIncr) {
        Promise.all(getterArr).then(() => {
          incrArr.length > 0 && this.increment(Counter, incrArr);
        })
      }

    },


    fetchData(api_server) {
      var Counter = (method, url, data) => {
        return fetch(`${ api_server }/1.1${ url }`, {
          method,
          headers: {
            'X-LC-Id': this.app_id,
            'X-LC-Key': this.app_key,
            'Content-Type': 'application/json',
          },
          body: JSON.stringify(data)
        });
      };
      this.addCount(Counter);
    },

    refreshCounter() {
      var api_server = this.app_id.slice(-9) !== '-MdYXbMMI' ? this.custom_api_server : `https://${ this.app_id.slice(0, 8).toLowerCase() }.api.lncldglobal.com`;
      if (api_server) {
        this.fetchData(api_server);
      } else {
        fetch('https://app-router.leancloud.cn/2/route?appId=' + this.app_id)
          .then(resp => resp.json())
          .then(({api_server}) => {
            this.fetchData('https://' + api_server);
          });
      }
    }

  };

  LCCounter.refreshCounter();

  document.addEventListener('pjax:complete', function () {
    LCCounter.refreshCounter();
  });
</script>




  <script>
const rootElement = document.documentElement;
const darkModeStorageKey = "user-color-scheme";
const rootElementDarkModeAttributeName = "data-user-color-scheme";

const setLS = (k, v) => {
    localStorage.setItem(k, v);
};

const removeLS = (k) => {
    localStorage.removeItem(k);
};

const getLS = (k) => {
    return localStorage.getItem(k);
};

const getModeFromCSSMediaQuery = () => {
  return window.matchMedia("(prefers-color-scheme: dark)").matches
    ? "dark"
    : "light";
};

const resetRootDarkModeAttributeAndLS = () => {
  rootElement.removeAttribute(rootElementDarkModeAttributeName);
  removeLS(darkModeStorageKey);
};

const validColorModeKeys = {
  dark: true,
  light: true,
};

const applyCustomDarkModeSettings = (mode) => {
  const currentSetting = mode || getLS(darkModeStorageKey);

  if (currentSetting === getModeFromCSSMediaQuery()) {
    resetRootDarkModeAttributeAndLS();
  } else if (validColorModeKeys[currentSetting]) {
    rootElement.setAttribute(rootElementDarkModeAttributeName, currentSetting);
  } else {
    resetRootDarkModeAttributeAndLS();
  }
};

const invertDarkModeObj = {
  dark: "light",
  light: "dark",
};

/**
 * get target mode
 */
const toggleCustomDarkMode = () => {
  let currentSetting = getLS(darkModeStorageKey);

  if (validColorModeKeys[currentSetting]) {
    currentSetting = invertDarkModeObj[currentSetting];
  } else if (currentSetting === null) {
    currentSetting = invertDarkModeObj[getModeFromCSSMediaQuery()];
  } else {
    return;
  }
  setLS(darkModeStorageKey, currentSetting);
  return currentSetting;
};

/**
 * bind click event for toggle button
 */
var btn=$("#wrapper .toggle-mode-btn,#rightmenu-wrapper .toggle-mode-btn");
function bindToggleButton() {
    btn.on('click',(e) => {
      const mode = toggleCustomDarkMode();
      applyCustomDarkModeSettings(mode);
    });
}

applyCustomDarkModeSettings();
document.addEventListener("DOMContentLoaded", bindToggleButton);
volantis.pjax.push(bindToggleButton);
volantis.pjax.send(()=>{
	btn.unbind('click');
},'toggle-mode-btn-unbind');
</script>








<script>
function listennSidebarTOC() {
  const navItems = document.querySelectorAll(".toc li");
  if (!navItems.length) return;
  const sections = [...navItems].map((element) => {
    const link = element.querySelector(".toc-link");
    const target = document.getElementById(
      decodeURI(link.getAttribute("href")).replace("#", "")
    );
    link.addEventListener("click", (event) => {
      event.preventDefault();
      window.scrollTo({
		top: target.offsetTop + 100,
		
		behavior: "smooth"
		
	  });
    });
    return target;
  });

  function activateNavByIndex(target) {
    if (target.classList.contains("active-current")) return;

    document.querySelectorAll(".toc .active").forEach((element) => {
      element.classList.remove("active", "active-current");
    });
    target.classList.add("active", "active-current");
    let parent = target.parentNode;
    while (!parent.matches(".toc")) {
      if (parent.matches("li")) parent.classList.add("active");
      parent = parent.parentNode;
    }
  }

  function findIndex(entries) {
    let index = 0;
    let entry = entries[index];
    if (entry.boundingClientRect.top > 0) {
      index = sections.indexOf(entry.target);
      return index === 0 ? 0 : index - 1;
    }
    for (; index < entries.length; index++) {
      if (entries[index].boundingClientRect.top <= 0) {
        entry = entries[index];
      } else {
        return sections.indexOf(entry.target);
      }
    }
    return sections.indexOf(entry.target);
  }

  function createIntersectionObserver(marginTop) {
    marginTop = Math.floor(marginTop + 10000);
    let intersectionObserver = new IntersectionObserver(
      (entries, observe) => {
        let scrollHeight = document.documentElement.scrollHeight + 100;
        if (scrollHeight > marginTop) {
          observe.disconnect();
          createIntersectionObserver(scrollHeight);
          return;
        }
        let index = findIndex(entries);
        activateNavByIndex(navItems[index]);
      },
      {
        rootMargin: marginTop + "px 0px -100% 0px",
        threshold: 0,
      }
    );
    sections.forEach((element) => {
      element && intersectionObserver.observe(element);
    });
  }
  createIntersectionObserver(document.documentElement.scrollHeight);
}

document.addEventListener("DOMContentLoaded", listennSidebarTOC);
document.addEventListener("pjax:success", listennSidebarTOC);
</script>

<!-- more -->

 
	   
	    


<script src="https://cdn.jsdelivr.net/npm/pjax@0.2.8/pjax.min.js"></script>


<script>
    var pjax;
    document.addEventListener('DOMContentLoaded', function () {
      pjax = new Pjax({
        elements: 'a[href]:not([href^="#"]):not([href="javascript:void(0)"]):not([pjax-fancybox])',
        selectors: [
          "title",
          
          "#pjax-container",
          "#pjax-header-nav-list"
        ],
        cacheBust: false,   // url 地址追加时间戳，用以避免浏览器缓存
        timeout: 5000
      });
    });

    document.addEventListener('pjax:send', function (e) {
      //window.stop(); // 相当于点击了浏览器的停止按钮

      try {
        var currentUrl = window.location.pathname;
        var targetUrl = e.triggerElement.href;
        var banUrl = [""];
        if (banUrl[0] != "") {
          banUrl.forEach(item => {
            if(currentUrl.indexOf(item) != -1 || targetUrl.indexOf(item) != -1) {
              window.location.href = targetUrl;
            }
          });
        }
      } catch (error) {}

      window.subData = null; // 移除标题（用于一二级导航栏切换处）

      volantis.$switcher.removeClass('active'); // 关闭移动端激活的搜索框
      volantis.$header.removeClass('z_search-open'); // 关闭移动端激活的搜索框
      volantis.$wrapper.removeClass('sub'); // 跳转页面时关闭二级导航

      // 解绑事件 避免重复监听
      volantis.$topBtn.unbind('click');
      $('.menu a').unbind('click');
      $(window).unbind('resize');
      $(window).unbind('scroll');
      $(document).unbind('scroll');
      $(document).unbind('click');
      $('body').unbind('click');
	  // 使用 volantis.pjax.send 方法传入pjax:send回调函数 参见layout/_partial/scripts/global.ejs
	  volantis.pjax.method.send.start();
    });

    document.addEventListener('pjax:complete', function () {
      $('.nav-main').find('.list-v').not('.menu-phone').removeAttr("style",""); // 移除小尾巴的移除
      $('.menu-phone.list-v').removeAttr("style",""); // 移除小尾巴的移除
      $('script[data-pjax], .pjax-reload script').each(function () {
        $(this).parent().append($(this).remove());
      });
      try{
		// 使用 volantis.pjax.push 方法传入重载函数 参见layout/_partial/scripts/global.ejs
		volantis.pjax.method.complete.start();
      } catch (e) {
        console.log(e);
      }
    });

    document.addEventListener('pjax:error', function (e) {
	  // 使用 volantis.pjax.error 方法传入pjax:error回调函数 参见layout/_partial/scripts/global.ejs
	  volantis.pjax.method.error.start();
      window.location.href = e.triggerElement.href;
    });
</script>
 
	  
    </div>
  </body>
</html>
